import { Command } from '../utils/cli-handler.js';
import { spawn } from 'child_process';
import { logger } from '../../core/logger.js';
import { Indent, cloudflareLogger, FLASHCORE_KEYS, HighlightGreen, Highlight, DEFAULT_CONFIG } from '../../core/constants.js';
import { loadConfig, loadConfigPath, getConfigPaths } from '../../core/config.js';
import { isCloudflaredInstalled, installCloudflared, startCloudflared, stopCloudflared } from '../utils/cloudflared.js';
import { packageJson, filterExistingPaths, getWatchedPlugins, timeout, IS_WINDOWS } from '../utils/utils.js';
import path from 'node:path';
import Watcher from '../utils/watcher.js';
import { color, composeColors } from '../../core/color.js';
import { Spirits } from '../utils/spirits.js';
import { buildAction } from './build/index.js';
import { prepareFlashcore, Flashcore } from '../../core/flashcore.js';
import { getPackageManager, getPackageExecutor } from '../utils/runtime-utils.js';
import { Mode, setMode } from '../../core/mode.js';
import { Compiler } from '../utils/compiler.js';
import { Env } from '../../core/env.js';

const command = new Command("dev").description("Ready, set, code your bot to life! Starts development mode.").option("-h", "--help", "Shows the available command options").option("-m", "--mode", "specify the mode(s) to run in (dev, beta, prod, etc...)").option("-s", "--silent", "do not print anything").option("-t", "--tunnel", "expose your local server to the internet").option("-v", "--verbose", "print more information for debugging").handler(devAction);
var dev_default = command;
let spirits;
async function devAction(_args, options) {
  logger({
    enabled: !options.silent,
    level: options.verbose ? "debug" : "info"
  });
  logger.debug("CLI options:", options);
  logger.debug(`Package manager:`, getPackageManager());
  logger.debug(`Robo.js version:`, packageJson.version);
  logger.debug(`Current working directory:`, process.cwd());
  if (!process.env.NODE_ENV) {
    process.env.NODE_ENV = "development";
  }
  const defaultMode = Mode.get();
  await Env.load({ mode: defaultMode });
  const { shardModes } = setMode(options.mode);
  if (shardModes) {
    return shardModes();
  }
  const projectName = path.basename(process.cwd()).toLowerCase();
  logger.log("");
  logger.log(Indent, color.bold(`\u{1F680} Starting ${color.cyan(projectName)} in ${Mode.color(Mode.get())} mode`));
  logger.log(Indent, "   Beep boop... Code your Robo to life! Got feedback? Tell us on Discord.");
  logger.log("");
  const config = await loadConfig("robo", true);
  const configPath = await loadConfigPath();
  let configRelative;
  if (configPath) {
    configRelative = path.relative(process.cwd(), configPath);
  } else {
    logger.error(`Could not find configuration file. Please make sure ${color.bold("/config/robo.mjs")} exists.`);
    process.exit(1);
  }
  const experimentalKeys = Object.entries(config.experimental ?? {}).filter(([, value]) => value).map(([key]) => key).filter((key) => key !== "disableBot");
  if (experimentalKeys.length > 0) {
    const features = experimentalKeys.map((key) => color.bold(key)).join(", ");
    logger.warn(`Experimental flags enabled: ${features}.`);
  }
  if (options.tunnel && !isCloudflaredInstalled()) {
    cloudflareLogger.event(`Installing Cloudflared...`);
    await installCloudflared();
    cloudflareLogger.info(`Cloudflared installed successfully!`);
  }
  if (options.tunnel && !process.env.PORT) {
    cloudflareLogger.error(`Cannot start tunnel without a PORT environment variable.`);
    process.exit(1);
  }
  spirits = new Spirits();
  let isStopping = false;
  let tunnelProcess;
  const callback = async (signal) => {
    if (isStopping) {
      return;
    }
    isStopping = true;
    await Promise.allSettled([spirits.stopAll(), stopCloudflared(tunnelProcess, signal)]);
    process.exit(0);
  };
  process.on("SIGINT", () => callback("SIGINT"));
  process.on("SIGTERM", () => callback("SIGTERM"));
  let buildSuccess = false;
  try {
    const start = Date.now();
    await buildAction([], {
      dev: true,
      verbose: options.verbose
    });
    logger.debug(`Build completed in ${Date.now() - start}ms`);
    buildSuccess = true;
  } catch (error) {
    logger.error(error);
  }
  let roboSpirit;
  const restartCallback = async (message) => {
    if (message.event === "restart" && message.payload === "trigger") {
      logger.wait(`Restarting Robo...`);
      spirits.off(roboSpirit, restartCallback);
      roboSpirit = await rebuildRobo(roboSpirit, config, options.verbose, []);
      spirits.on(roboSpirit, restartCallback);
    }
  };
  const stateStart = Date.now();
  await prepareFlashcore();
  const persistedState = await Flashcore.get(FLASHCORE_KEYS.state) ?? {};
  logger.debug(`State loaded in ${Date.now() - stateStart}ms`);
  if (buildSuccess) {
    roboSpirit = await spirits.newTask({
      event: "start",
      onExit: (exitCode) => {
        if (exitCode !== 0) {
          logger.error(
            composeColors(
              color.bgBlack,
              color.redBright,
              color.underline,
              color.bold
            )(`Robo exited with code ${exitCode}`)
          );
          return false;
        }
      },
      onRetry: (value) => {
        roboSpirit = value;
        spirits.on(roboSpirit, restartCallback);
        spirits.send(roboSpirit, { event: "set-state", state: persistedState });
      }
    });
    spirits.on(roboSpirit, restartCallback);
    spirits.send(roboSpirit, { event: "set-state", state: persistedState });
  } else {
    logger.wait(`Build failed! Waiting for changes before retrying...`);
  }
  let manifest = await Compiler.useManifest();
  const watchedPaths = ["src"];
  const ignoredPaths = ["node_modules", ".git", ...config.watcher?.ignore ?? []];
  const additionalFiles = await filterExistingPaths([".env", "tsconfig.json"]);
  watchedPaths.push(...additionalFiles);
  const watchedPlugins = await getWatchedPlugins(config);
  Object.keys(watchedPlugins).forEach((pluginPath) => watchedPaths.push(pluginPath));
  getConfigPaths().forEach((configPath2) => watchedPaths.push(path.relative(process.cwd(), configPath2)));
  logger.debug(`Watching:`, watchedPaths);
  logger.debug(`Ignoring paths:`, ignoredPaths);
  const watcher = new Watcher(watchedPaths, { exclude: ignoredPaths });
  let isUpdating = false;
  watcher.start(async (changes) => {
    logger.debug("Watcher events:", changes);
    if (isUpdating) {
      return logger.debug(`Already updating, skipping...`);
    }
    isUpdating = true;
    try {
      const configChange = changes.find((change) => change.filePath === configRelative);
      const pluginChange = changes.find((change) => Object.keys(watchedPlugins).includes(change.filePath));
      if (configChange) {
        const fileName = configChange.filePath.split("/").pop();
        logger.wait(`${color.bold(fileName)} file was updated. Restarting to apply configuration...`);
      } else if (pluginChange) {
        const plugin = watchedPlugins[pluginChange.filePath];
        logger.wait(`${color.bold(plugin.name)} plugin was updated. Restarting to apply changes...`);
      } else {
        logger.wait(`Change detected. Restarting Robo...`);
      }
      roboSpirit = await rebuildRobo(roboSpirit, config, options.verbose, changes);
      spirits.on(roboSpirit, restartCallback);
      if (config.experimental?.disableBot !== true) {
        const newManifest = await Compiler.useManifest();
        const oldPermissions = manifest.permissions ?? [];
        const newPermissions = newManifest.permissions ?? [];
        manifest = newManifest;
        if (JSON.stringify(oldPermissions) !== JSON.stringify(newPermissions)) {
          logger.warn(
            `Permissions have changed! Run ${color.bold("robo invite")} to update your Robo's guild permissions.`
          );
        }
      }
    } finally {
      isUpdating = false;
    }
  });
  if (options.tunnel) {
    tunnelProcess = startCloudflared("http://localhost:" + process.env.PORT);
  }
  try {
    await checkUpdates(config);
  } catch (error) {
    logger.warn(error);
  }
}
async function buildAsync(command2, config, verbose, changes) {
  return new Promise((resolve, reject) => {
    const start = Date.now();
    if (!command2) {
      spirits.newTask({
        event: "build",
        payload: {
          files: changes.map((change) => change.filePath),
          mode: Mode.get()
        },
        verbose
      }).then(() => {
        logger.debug(`Build completed in ${Date.now() - start}ms`);
        resolve(true);
      }).catch(() => resolve(false));
    } else {
      const args = command2.split(" ");
      let pkgManager = getPackageManager();
      if (pkgManager === "pnpm" && IS_WINDOWS) {
        logger.debug(`Detected Windows. Using ${color.bold("npm")} instead of ${color.bold("pnpm")} to build.`);
        pkgManager = "npm";
      }
      if (pkgManager === "npm" || pkgManager === "pnpm") {
        args.splice(0, 0, "exec");
      }
      if (pkgManager === "npm") {
        const optionsIndex = args.findIndex((arg) => arg.startsWith("-"));
        if (optionsIndex !== -1) {
          args.splice(optionsIndex, 0, "--");
        }
      }
      logger.debug(`> ${pkgManager} ${args.join(" ")}`);
      const childProcess = spawn(pkgManager, args, {
        env: { ...process.env, FORCE_COLOR: "1" },
        shell: IS_WINDOWS,
        stdio: "inherit"
      });
      childProcess.on("close", (code) => {
        if (code === 0) {
          logger.debug(`Build completed in ${Date.now() - start}ms`);
          resolve(true);
        } else {
          resolve(false);
        }
      });
      childProcess.on("error", (error) => {
        reject(error);
        resolve(false);
      });
    }
  });
}
async function checkUpdates(config, forceCheck = false, suggest = true) {
  const { updateCheckInterval = 60 * 60 } = config;
  const update = {
    currentVersion: packageJson.version,
    hasUpdate: false,
    latestVersion: ""
  };
  if (!forceCheck && updateCheckInterval <= 0) {
    return update;
  }
  const lastUpdateCheck = await Flashcore.get(FLASHCORE_KEYS.lastUpdateCheck) ?? 0;
  const now = Date.now();
  const isDue = now - lastUpdateCheck > updateCheckInterval * 1e3;
  if (!forceCheck && !isDue) {
    return update;
  }
  const response = await fetch(`https://registry.npmjs.org/${packageJson.name}/latest`);
  const latestVersion = (await response.json()).version;
  update.hasUpdate = packageJson.version !== latestVersion;
  update.latestVersion = latestVersion;
  await Flashcore.set(FLASHCORE_KEYS.lastUpdateCheck, now);
  if (update.hasUpdate) {
    const packageExecutor = getPackageExecutor();
    const versionDelta = color.dim(`(v${packageJson.version} \u2192 v${latestVersion})`);
    let message = `
${Indent} \u{1F4A1} ${HighlightGreen("Update available!")} ${versionDelta}
`;
    if (suggest) {
      const command2 = `${packageExecutor} sage upgrade`;
      message += `${Indent}    Run ${Highlight(command2)} or disable update checks in config.
`;
    }
    logger.log(message);
  }
  return update;
}
async function rebuildRobo(spiritId, config, verbose, changes) {
  const roboSpirit = spiritId;
  const isValid = roboSpirit !== null && roboSpirit !== void 0;
  const stateSaveStart = Date.now();
  logger.debug("Saving state...");
  const savedState = await spirits.exec(roboSpirit, {
    event: "get-state"
  }) ?? {};
  logger.debug(`Saved state in ${Date.now() - stateSaveStart}ms:`, savedState);
  let isTerminated = false;
  const terminate = new Promise((resolve) => {
    if (!isValid) {
      return resolve();
    }
    const spirit = spirits.get(roboSpirit);
    const callback = () => {
      logger.debug(`Gracefully stopped Robo spirit (${composeColors(color.bold, color.cyan)(roboSpirit)})`);
      spirit.worker?.off("exit", callback);
      spirit.isTerminated = true;
      isTerminated = true;
      resolve();
    };
    if (spirit?.isTerminated) {
      return callback();
    }
    spirit.worker.once("exit", callback);
    spirit.worker.on("message", (message) => {
      if (message.payload === "exit") {
        callback();
      }
    });
    spirits.send(roboSpirit, { event: "restart", verbose });
  });
  const forceAbort = timeout(() => {
    if (!isTerminated && isValid) {
      logger.warn("Robo termination timed out. Force stopping...");
      spirits.stop(roboSpirit, true);
    }
  }, config?.timeouts?.lifecycle ?? DEFAULT_CONFIG.timeouts.lifecycle);
  const awaitStop = Promise.race([terminate, forceAbort]);
  const [success] = await Promise.all([buildAsync(null, config, verbose, changes), awaitStop]);
  if (!success) {
    logger.wait(`Build failed! Waiting for changes before retrying...`);
    return null;
  }
  const start = Date.now();
  const newSpiritId = await spirits.newTask({ event: "start" });
  logger.debug(`Robo spirit (${composeColors(color.bold, color.cyan)(newSpiritId)}) started in ${Date.now() - start}ms`);
  spirits.send(newSpiritId, { event: "set-state", state: savedState });
  return newSpiritId;
}

export { buildAsync, checkUpdates, dev_default as default };
